Skip to main content

02 - 两套语义约定

前置01 篇的操作类型与指标。

本篇回答:同一件事为什么存在两套规范、各自覆盖什么、以及怎么让这个选择在未来可以反悔。

本篇会用到的词

意思
命名空间(前缀)属性名开头那一段,比如 gen_ai.usage.input_tokens 里的 gen_ai。两套规范争的就是这一段该叫什么
OTLPOpenTelemetry Protocol,OpenTelemetry 定义的数据传输协议。两套语义约定都走它,所以传输层不是它们的分歧所在
vendor-neutral厂商中立 —— 数据不绑定在某一家产品上,换后端时能带走。这通常是合规要求,不是技术偏好
仪表化库(instrumentation)替你自动产生 span 的现成库,装上就能给 OpenAI SDK、LangChain 这些常用组件自动埋点,不用手写
Stable / Development规范里给每个字段标的稳定性等级。Development 意味着随时可能改名或删掉,本篇的核心结论就跟这个标记有关
翻译层后端把外来格式映射进自己数据模型的那一层。它让一个后端能同时吃两套约定,但吃进去之后落到的字段不一定相同

一、两套规范的定位

OpenTelemetry GenAIOpenInference
仓库open-telemetry/semantic-conventions-genaiArize-ai/openinference
协议Apache-2.0Apache-2.0
属性前缀gen_ai.*llm.* document.* embedding.*
出身OpenTelemetry 官方Arize(Phoenix 的开发方)
定位通用可观测标准的 GenAI 扩展面向 LLM 应用调试的实用约定
关于用 star 数比较这两个项目

规范类仓库的 star 数没有比较意义 —— 使用者 star 的是 SDK 和平台,不是规范文本。判断采纳度应看哪些平台和网关实现了它,见第三节。

二、覆盖范围不同

两套的差异不在命名风格,而在建模粒度

差异不在命名风格,在建模粒度 —— 各自把哪一块做细了OTel GenAI模型调用:chat · embeddings · token 用量 · 延迟Agent 生命周期:invoke_agent · plan · execute_tool检索:只有一个 retrieval span,粒度较粗OpenInference模型调用:llm.* 命名空间检索链路:document.* 逐文档记 id · score · 内容向量化:embedding.* 独立建模深色格子是各自的强项。做 Agent 编排调试,前者的 span 类型更全;做 RAG 效果排查,后者能看到每一篇召回文档的得分。
这个差异直接来自出身:OTel GenAI 走的是通用可观测性路线,OpenInference 出自做 ML 可观测性的 Arize,评测和检索质量在他们的世界里本来就是一等公民。

OTel GenAI 在 Agent 编排侧更完整(有 invoke_agentplaninvoke_workflow)。

OpenInference 在 RAG 侧更细document.* 可以逐条记录召回的文档 id、相关性分数和内容片段 —— 调试"为什么召回了不相关的东西"时,这个粒度是必需的。

三、生态站队

平台采用的约定说明
Arize Phoenix(★11,105)OpenInference自家的 llm.* 命名空间,靠翻译层兼容其他
Langfuse(★33,369)自有数据模型接受 OTLP,gen_ai.* 和 OpenInference 都映射进自己的模型
OpenLLMetry(★7,384)OTel 风格Apache-2.0,仪表化库
Envoy AI GatewayOpenInference见下

3.1 Envoy AI Gateway 的选择

Envoy AI Gateway 的源码里有一个完整的 internal/tracing/openinference/ 目录:

internal/tracing/openinference/openai/request_attrs.go    38 KB
internal/tracing/openinference/openai/response_attrs.go 21 KB

一个 CNCF 生态的项目,在追踪层选了非 OTel 官方的约定。 这不是偶然 —— 对网关来说,能否把请求和响应的细节结构化地记下来,比命名是否"官方"更重要,而 OpenInference 在这方面更成熟。

这是判断采纳度比 star 数可靠得多的信号:看基础设施项目在生产路径上用了谁。

四、真正的建议:不要在业务代码里选边

两套都在演进,OTel GenAI 甚至尚未稳定(01 篇 5 节)。在业务代码里直接写属性名,等于把规范的不确定性扩散到每一个调用点。

# ❌ 业务代码直接依赖某一套约定
# 换约定 = 全局搜索替换,且两套并存时无法处理
span.set_attribute("gen_ai.usage.input_tokens", n)
span.set_attribute("gen_ai.request.model", model)

# ✅ 收敛到一个转换层,由它决定输出哪一套(或同时输出两套)
class SpanAttrs:
"""埋点属性的唯一出口。

业务代码只调用语义化方法,不接触具体属性名。
切换约定、同时输出两套、适配规范变更,都只改这个类。
"""

def __init__(self, span, dialect: str = "otel"):
self._span = span
self._dialect = dialect # "otel" | "openinference" | "both"

def input_tokens(self, n: int) -> None:
if self._dialect in ("otel", "both"):
self._span.set_attribute("gen_ai.usage.input_tokens", n)
if self._dialect in ("openinference", "both"):
self._span.set_attribute("llm.token_count.prompt", n)

def model(self, name: str) -> None:
if self._dialect in ("otel", "both"):
self._span.set_attribute("gen_ai.request.model", name)
if self._dialect in ("openinference", "both"):
self._span.set_attribute("llm.model_name", name)

同时输出两套的额外成本很低(每个 span 多几个属性),换来的是后端可以随时更换。在规范未稳定期间,这个交换是划算的。

五、两个常见误解

5.1 以为选了后端就等于选了约定

后端和约定是两个独立的选择:

六种组合全都成立 —— 所以这是两个可以分开做的决定埋点用哪套约定OTel GenAIOpenInferenceOTLP通用传输协议,两套都能走数据发往哪个后端Langfuse两套约定都映射进自有数据模型Phoenix原生 OpenInference,另一套走翻译层自建 OTel 后端收什么存什么后端能同时吃两套,不等于两套数据在后端里长得一样 —— 混用的系统最后一定会出现「某些请求成本统计为 0」这类现象。团队内必须统一。
把这两件事分开想,选型压力会小很多:埋点是最难改的部分,后端是最容易换的部分,所以在埋点上要选最中立的方案,后端可以随阶段调整。

Langfuse 两套都接,Phoenix 主推 OpenInference 但有翻译层。先定约定,再定后端 —— 反过来会被后端锁死。

5.2 以为标准稳定了就不用管

GenAI 约定在 2026 年发生过一次仓库迁移(v1.42.0,2026-06-12,gen_ai.* 拆出主仓库)。属性名在稳定之前仍可能变化。

应对方式就是第四节的转换层 —— 变更收敛到一处。

下一篇03 - 成本归因与选型

← 回到 专题索引  ·  Agent Infra 板块总览